/**
 * Aptana Studio
 * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
 * Please see the license.html included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.editor.common.contentassist;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.texteditor.ChainedPreferenceStore;
import org.osgi.service.prefs.BackingStoreException;

import com.aptana.core.CorePlugin;
import com.aptana.core.IUserAgent;
import com.aptana.core.IUserAgentManager;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.ArrayUtil;
import com.aptana.core.util.ResourceUtil;
import com.aptana.core.util.StringUtil;
import com.aptana.editor.common.CommonEditorPlugin;
import com.aptana.editor.common.IDebugScopes;
import com.aptana.ui.epl.UIEplPlugin;

public class UserAgentManager implements IUserAgentManager
{

	/**
	 * A reference to the singleton UserAgentManager object
	 */
	private static UserAgentManager INSTANCE;

	/**
	 * The delimiter used between a list of user agent ids in the preference value
	 */
	private static final String USER_AGENT_DELIMITER = ","; //$NON-NLS-1$

	/**
	 * The delimiter used between each entries where an entry defines a nature and its active user agents
	 */
	private static final String ENTRY_DELIMITER = ";"; //$NON-NLS-1$

	/**
	 * The delimiter used between the nature and the user agent list in a entry
	 */
	private static final String NAME_VALUE_SEPARATOR = ":"; //$NON-NLS-1$

	/**
	 * A mapping of nature ID to an array of user agent ids. The user agent id array contains the ids which are
	 * currently active for the given nature. This is mainly a in-memory cache of the string representation in the
	 * preference key. This structure is generated by {@link #loadPreference()} and is converted to a preference key
	 * value via {@link #savePreference()}
	 */
	private static Map<String, String[]> ACTIVE_USER_AGENTS_BY_NATURE_ID;

	/**
	 * Grab the singleton instance of the UserAgentManager. This method is responsible for creating the instance if it
	 * does not exist. The instance is initialized (processes extension point and preference key) when it is created
	 * 
	 * @return Returns the singleton instance of this class
	 */
	public synchronized static UserAgentManager getInstance()
	{
		if (INSTANCE == null)
		{
			INSTANCE = new UserAgentManager();
			INSTANCE.loadPreference();
		}

		return INSTANCE;
	}

	/**
	 * A registry of images used to maintain icons for each user agent
	 */
	private ImageRegistry imageRegistry;

	/**
	 * Make sure no one accept the class itself can instantiate this class
	 */
	private UserAgentManager()
	{
	}

	/**
	 * Given a project, return the list of active user agent IDs. The current implementation processes only the first
	 * (primary) nature ID of the given project. Secondary natures may be taken into consideration at a later point in
	 * time
	 * 
	 * @param project
	 *            An {@link IProject}.
	 * @return Returns an array of user agent IDs for the main mature of the given project. In case the given project is
	 *         null, an empty string array is returned.
	 */
	public String[] getActiveUserAgentIDs(IProject project)
	{
		if (project == null)
		{
			return ArrayUtil.NO_STRINGS;
		}
		// Extract the natures from the given project
		String[] natureIDs = getProjectNatures(project);

		// Look at the project-scope preferences for the active agents.
		ProjectScope scope = new ProjectScope(project);
		IEclipsePreferences node = scope.getNode(CommonEditorPlugin.PLUGIN_ID);
		if (node != null)
		{
			String agents = node.get(IPreferenceConstants.USER_AGENT_PREFERENCE, null);
			if (agents != null)
			{
				Map<String, String[]> userAgents = extractUserAgents(agents);
				return getActiveUserAgentIDs(userAgents, natureIDs);
			}
		}
		// In case we did not find any project-specific settings, use the project's nature IDs to grab the agents that
		// were set in the workspace settings.
		return getActiveUserAgentIDs(natureIDs);
	}

	/**
	 * Extract the aptana project natures.
	 * 
	 * @param project
	 * @return An array of project nature ids.
	 */
	private static String[] getProjectNatures(IProject project)
	{
		String[] natureIDs = ArrayUtil.NO_STRINGS;
		try
		{
			natureIDs = ResourceUtil.getAptanaNatures(project.getDescription());
		}
		catch (CoreException e)
		{
			IdeLog.logWarning(CommonEditorPlugin.getDefault(), "Problem detecting the project's nature IDs for " //$NON-NLS-1$
					+ project.getName(), e, IDebugScopes.CONTENT_ASSIST);
		}
		return natureIDs;
	}

	/**
	 * Given a list of nature IDs, return the list of active user agent IDs. The current implementation processes only
	 * the first (primary) nature ID. The signature allows for multiple nature IDs in case secondary natures need to be
	 * taken into consideration at a later point in time
	 * 
	 * @param natureIDs
	 *            An array of nature IDs to process
	 * @return Returns an array of user agent IDs
	 */
	private String[] getActiveUserAgentIDs(String... natureIDs)
	{
		return getActiveUserAgentIDs(ACTIVE_USER_AGENTS_BY_NATURE_ID, natureIDs);
	}

	/**
	 * Given a map of nature-ids to user-agents, and a list of nature-ids, return the matching user-agents.
	 * 
	 * @param userAgents
	 * @param natureIDs
	 */
	private String[] getActiveUserAgentIDs(Map<String, String[]> userAgents, String... natureIDs)
	{
		if (!ArrayUtil.isEmpty(natureIDs))
		{
			// NOTE: We now loop through the natures and find the first match. if no matches, return empty array.

			for (String natureID : natureIDs)
			{
				if (userAgents.containsKey(natureID))
				{
					return userAgents.get(natureID);
				}
			}
		}

		IdeLog.logWarning(CommonEditorPlugin.getDefault(), "UserAgentManager - Got empty natures list", //$NON-NLS-1$
				IDebugScopes.CONTENT_ASSIST);
		return ArrayUtil.NO_STRINGS;
	}

	/**
	 * Returns an array of UserAgent instances for a given an {@link IProject}. This method uses
	 * {@link #getActiveUserAgentIDs(IProject)} and therefore has the same limitations on natureIDs as described there
	 * 
	 * @return Returns an array array of UserAgent instances
	 */
	public IUserAgent[] getActiveUserAgents(IProject project)
	{
		return getUserAgentsByID(getActiveUserAgentIDs(project));
	}

	/**
	 * Returns an array of UserAgent instances for a given list of nature IDs. This method uses
	 * {@link #getActiveUserAgentIDs(String...)} and therefore has the same limitations on natureIDs as described there
	 * 
	 * @return Returns an array array of UserAgent instances
	 */
	public IUserAgent[] getActiveUserAgents(String... natureIDs)
	{
		return getUserAgentsByID(getActiveUserAgentIDs(natureIDs));
	}

	/**
	 * Returns an array of UserAgents for all user agents known at runtime
	 * 
	 * @return Returns an array of UserAgent instances
	 */
	public IUserAgent[] getAllUserAgents()
	{
		return CorePlugin.getDefault().getUserAgentManager().getAllUserAgents();
	}

	public boolean addUserAgent(IUserAgent agent)
	{
		return CorePlugin.getDefault().getUserAgentManager().addUserAgent(agent);
	}

	/**
	 * Returns a list of user agent IDs which are the default IDs for the specified nature. This method is typically
	 * used in part to reset the list of active user agents for a given nature ID, particularly in the Content Assist
	 * preference page.
	 * 
	 * @param natureID
	 *            The nature ID to use when looking up the default user agent ID list
	 * @return Returns an array of user agent IDs
	 * @deprecated
	 */
	public String[] getDefaultUserAgentIDs(String natureID)
	{
		IUserAgent[] agents = getDefaultUserAgents(natureID);
		String[] result = new String[agents.length];
		for (int i = 0; i < agents.length; i++)
		{
			result[i] = agents[i].getID();
		}
		return result;
	}

	public IUserAgent[] getDefaultUserAgents(String natureID)
	{
		return CorePlugin.getDefault().getUserAgentManager().getDefaultUserAgents(natureID);
	}

	public Image getDisabledIcon(IUserAgent agent)
	{
		return getImage(agent, agent.getDisabledIconPath());
	}

	public Image getEnabledIcon(IUserAgent agent)
	{
		return getImage(agent, agent.getEnabledIconPath());
	}

	private Image getImage(IUserAgent agent, String iconPath)
	{
		Image result = null;

		if (iconPath != null)
		{
			if (imageRegistry == null)
			{
				imageRegistry = new ImageRegistry();
			}
			String key = agent.getID() + '_' + iconPath;
			result = imageRegistry.get(key);
			if (result == null)
			{
				ImageDescriptor desc = CommonEditorPlugin.imageDescriptorFromPlugin(agent.getContributor(), iconPath);
				imageRegistry.put(key, desc);
			}
			result = imageRegistry.get(key);
		}

		return result;
	}

	/**
	 * Return an array of icons, one for each user agent ID in the userAgents array. The specified project provides a
	 * list of natures that is used to determine the list of active user agents via
	 * {@link #getActiveUserAgents(IProject)}. These user agents are compared to the user agents passed into this
	 * method. All user agents that are not in the specified array will return disabled icons. All others return enabled
	 * icons.
	 * 
	 * @param project
	 * @param userAgents
	 *            An array of user agent IDs
	 * @return Returns an array of Images
	 */
	public Image[] getUserAgentImages(IProject project, String... userAgents)
	{
		IUserAgent[] activeUserAgents = getActiveUserAgents(project);
		Set<String> enabledAgents;
		if (userAgents == null)
		{
			enabledAgents = Collections.emptySet();
		}
		else
		{
			enabledAgents = new HashSet<String>(Arrays.asList(userAgents));
		}
		Image[] result = new Image[activeUserAgents.length];

		Arrays.sort(activeUserAgents);

		for (int i = 0; i < activeUserAgents.length; i++)
		{
			IUserAgent userAgent = activeUserAgents[i];

			if (userAgent != null)
			{
				boolean isEnabled = enabledAgents.contains(userAgent.getID());
				result[i] = isEnabled ? getEnabledIcon(userAgent) : getDisabledIcon(userAgent);
			}
		}

		return result;
	}

	/**
	 * Return an array of UserAgents, one for each recognized user agent ID
	 * 
	 * @param ids
	 *            An array of user agent ids
	 * @return Returns an array of UserAgent instances
	 */
	private IUserAgent[] getUserAgentsByID(String... ids)
	{
		List<IUserAgent> result = new ArrayList<IUserAgent>();

		if (ids != null && ids.length > 0)
		{
			for (String id : ids)
			{
				IUserAgent userAgent = getUserAgentById(id);
				if (userAgent != null)
				{
					result.add(userAgent);
				}
			}
		}

		return result.toArray(new IUserAgent[result.size()]);
	}

	public IUserAgent getUserAgentById(String id)
	{
		return CorePlugin.getDefault().getUserAgentManager().getUserAgentById(id);
	}

	/**
	 * Process the preference key value associated with UserAgentManager. This method is automatically called when the
	 * singleton instance is created. If the preference key value is somehow manipulated outside of UserAgentManager
	 * (and it shouldn't be), then this method will need to be invoked to update the in-memory cache of
	 * nature/user-agent info.
	 */
	void loadPreference()
	{
		// Grab preference value. We use a ChainedPreferenceStore to be able to migrate the preference location from the
		// UIEplPlugin to the CommonEditorPlugin (the new location that we will use to save the
		// IPreferenceConstants.USER_AGENT_PREFERENCE)
		ChainedPreferenceStore chainedStore = new ChainedPreferenceStore(new IPreferenceStore[] {
				CommonEditorPlugin.getDefault().getPreferenceStore(), UIEplPlugin.getDefault().getPreferenceStore() });
		String preferenceValue = chainedStore.getString(IPreferenceConstants.USER_AGENT_PREFERENCE);

		Map<String, String[]> result;
		if (!StringUtil.isEmpty(preferenceValue))
		{
			result = extractUserAgents(preferenceValue);
		}
		else
		{
			result = new HashMap<String, String[]>();
			// set defaults
			for (String natureID : ResourceUtil.getAptanaNaturesMap().values())
			{
				result.put(natureID, getDefaultUserAgentIDs(natureID));
			}
		}

		// cache result
		ACTIVE_USER_AGENTS_BY_NATURE_ID = result;
	}

	private Map<String, String[]> extractUserAgents(String preferenceValue)
	{
		Map<String, String[]> result = new HashMap<String, String[]>();
		if (preferenceValue.contains(NAME_VALUE_SEPARATOR))
		{
			// looks like the latest format for this pref key
			String[] entries = preferenceValue.split(ENTRY_DELIMITER);

			for (String entry : entries)
			{
				String[] nameValue = entry.split(NAME_VALUE_SEPARATOR);
				String natureID = nameValue[0];

				if (nameValue.length > 1)
				{
					String userAgentIDsString = nameValue[1];
					String[] userAgentIDs = userAgentIDsString.split(USER_AGENT_DELIMITER);

					result.put(natureID, userAgentIDs);
				}
				else
				{
					result.put(natureID, ArrayUtil.NO_STRINGS);
				}
			}
		}
		else
		{
			// assume this is an old style preference and update each nature ID with its user agent settings

			// NOTE: We don't manipulate these arrays directly, so it should be fine to reference the same
			// array for all natures
			String[] userAgentIDs = preferenceValue.split(USER_AGENT_DELIMITER);

			for (String natureID : ResourceUtil.getAptanaNaturesMap().values())
			{
				result.put(natureID, userAgentIDs);
			}
		}
		return result;
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.IUserAgentPreferenceManager#savePreference()
	 */
	public void savePreference()
	{
		savePreference(null, ACTIVE_USER_AGENTS_BY_NATURE_ID);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * com.aptana.editor.common.contentassist.IUserAgentPreferenceManager#savePreference(org.eclipse.core.resources.
	 * IProject, java.util.Map)
	 */
	public void savePreference(IProject project, Map<String, String[]> natureIdToUserAgents)
	{
		IEclipsePreferences preferences = null;
		if (project != null)
		{
			// Save to the project scope
			preferences = new ProjectScope(project).getNode(CommonEditorPlugin.PLUGIN_ID);
		}
		else
		{
			// Save to the instance scope (plugin)
			preferences = InstanceScope.INSTANCE.getNode(CommonEditorPlugin.PLUGIN_ID);
		}

		// convert active user agents to a string representation
		List<String> natureEntries = new ArrayList<String>();

		for (Map.Entry<String, String[]> entry : natureIdToUserAgents.entrySet())
		{
			String natureID = entry.getKey();
			String userAgentIDs = StringUtil.join(USER_AGENT_DELIMITER, entry.getValue());
			natureEntries.add(natureID + NAME_VALUE_SEPARATOR + userAgentIDs);
		}

		String value = StringUtil.join(ENTRY_DELIMITER, natureEntries);

		// save value
		if (preferences != null)
		{
			preferences.put(IPreferenceConstants.USER_AGENT_PREFERENCE, value);
			try
			{
				preferences.flush();
			}
			catch (BackingStoreException e)
			{
				IdeLog.logWarning(CommonEditorPlugin.getDefault(), "Error saving the user-agent preferences.", e); //$NON-NLS-1$
			}
		}
		else
		{
			IdeLog.logError(CommonEditorPlugin.getDefault(),
					"Error saving the user-agent preferences. Preferences node was null"); //$NON-NLS-1$
		}
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * com.aptana.editor.common.contentassist.IUserAgentPreferenceManager#clearPreferences(org.eclipse.core.resources
	 * .IProject)
	 */
	public void clearPreferences(IProject project)
	{
		if (project != null)
		{
			// Save to the project scope
			IEclipsePreferences preferences = new ProjectScope(project).getNode(CommonEditorPlugin.PLUGIN_ID);
			preferences.remove(IPreferenceConstants.USER_AGENT_PREFERENCE);
			try
			{
				preferences.flush();
			}
			catch (BackingStoreException e)
			{
				// ignore
			}
		}
	}

	/**
	 * Set the currently active list of user agent IDs for the given nature ID. This updates the UserAgentMemory
	 * in-memory cache. To persist these changes, {@link #savePreference()} will need to be called. This allows for
	 * multiple changes to be made with a final single write to the preference key.
	 * 
	 * @param userAgents
	 */
	public void setActiveUserAgents(String natureID, String[] userAgentIDs)
	{
		if (!StringUtil.isEmpty(natureID))
		{
			String[] value = (userAgentIDs != null) ? userAgentIDs : ArrayUtil.NO_STRINGS;

			ACTIVE_USER_AGENTS_BY_NATURE_ID.put(natureID, value);
		}
	}
}
